// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "as370-usb-phy.h"

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/platform-defs.h>
#include <soc/as370/as370-reset.h>
#include <soc/as370/as370-usb.h>
#include <fbl/algorithm.h>
#include <fbl/unique_ptr.h>
#include <hw/reg.h>

namespace as370_usb_phy {

void UsbPhy::ResetPhy() {
  auto* mmio = &*reset_mmio_;

  auto reset = as370::GblPerifStickyResetN::Get().ReadFrom(mmio);
  reset.set_usbOtgPhyreset(0).WriteTo(mmio);
  reset.set_usbOtgPrstn(1).WriteTo(mmio);
  usleep(10);
  reset.set_usbOtgHresetn(1).WriteTo(mmio);
  usleep(100);
}

zx_status_t UsbPhy::InitPhy() {
  auto* mmio = &*usbphy_mmio_;

  as370::USB_PHY_CTRL0::Get().FromValue(0).set_value(0x0EB35E84).WriteTo(mmio);
  as370::USB_PHY_CTRL1::Get().FromValue(0).set_value(0x80E9F004).WriteTo(mmio);

  ResetPhy();

  uint32_t count = 10000;
  while (count) {
    if (as370::USB_PHY_RB::Get().ReadFrom(mmio).clk_rdy()) {
      break;
    }
    usleep(1);
    count--;
  }

  return count ? 0 : ZX_ERR_TIMED_OUT;

  return ZX_OK;
}

zx_status_t UsbPhy::Create(void* ctx, zx_device_t* parent) {
  auto dev = std::make_unique<UsbPhy>(parent);
  auto status = dev->Init();
  if (status != ZX_OK) {
    return status;
  }

  // devmgr is now in charge of the device.
  __UNUSED auto* dummy = dev.release();
  return ZX_OK;
}

zx_status_t UsbPhy::AddDwc2Device() {
  if (dwc2_device_) {
    zxlogf(ERROR, "UsbPhy::AddDwc2Device: device already exists!\n");
    return ZX_ERR_BAD_STATE;
  }

  fbl::AllocChecker ac;
  dwc2_device_ = fbl::make_unique_checked<Dwc2Device>(&ac, zxdev());
  if (!ac.check()) {
    return ZX_ERR_NO_MEMORY;
  }

  zx_device_prop_t props[] = {
      {BIND_PLATFORM_DEV_VID, 0, PDEV_VID_GENERIC},
      {BIND_PLATFORM_DEV_PID, 0, PDEV_PID_GENERIC},
      {BIND_PLATFORM_DEV_DID, 0, PDEV_DID_USB_DWC2},
  };

  return dwc2_device_->DdkAdd("dwc2", 0, props, countof(props), ZX_PROTOCOL_USB_PHY);
}

zx_status_t UsbPhy::RemoveDwc2Device() {
  if (dwc2_device_ == nullptr) {
    zxlogf(ERROR, "UsbPhy::RemoveDwc2Device: device does not exist!\n");
    return ZX_ERR_BAD_STATE;
  }

  // devmgr will own the device until it is destroyed.
  __UNUSED auto* dev = dwc2_device_.release();
  dev->DdkRemove();

  return ZX_OK;
}

zx_status_t UsbPhy::Init() {
  if (!pdev_.is_valid()) {
    zxlogf(ERROR, "UsbPhy::Init: could not get platform device protocol\n");
    return ZX_ERR_NOT_SUPPORTED;
  }

  auto status = pdev_.MapMmio(0, &usbphy_mmio_);
  if (status != ZX_OK) {
    zxlogf(ERROR, "UsbPhy::Init: MapMmio failed for usbphy_mmio_\n");
    return status;
  }
  status = pdev_.MapMmio(1, &reset_mmio_);
  if (status != ZX_OK) {
    zxlogf(ERROR, "UsbPhy::Init: MapMmio failed for reset_mmio_\n");
    return status;
  }

  status = InitPhy();
  if (status != ZX_OK) {
    zxlogf(ERROR, "UsbPhy::Init: InitPhy() failed\n");
    return status;
  }

  status = DdkAdd("as370-usb-phy", DEVICE_ADD_NON_BINDABLE);
  if (status != ZX_OK) {
    zxlogf(ERROR, "UsbPhy::Init: DdkAdd() failed\n");
    return status;
  }

  AddDwc2Device();

  return ZX_OK;
}

void UsbPhy::DdkUnbind() {
  RemoveDwc2Device();
  DdkRemove();
}

void UsbPhy::DdkRelease() { delete this; }

static constexpr zx_driver_ops_t driver_ops = []() {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = UsbPhy::Create;
  return ops;
}();

}  // namespace as370_usb_phy

ZIRCON_DRIVER_BEGIN(as370_usb_phy, as370_usb_phy::driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PDEV),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_SYNAPTICS),
    BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_SYNAPTICS_AS370),
    BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_AS370_USB_PHY), ZIRCON_DRIVER_END(as370_usb_phy)
